// -------------------------------------------------------------------------------------
// ExecMonitor
// -------------------------------------------------------------------------------------
// Permission is granted to freely redistribute this source code, and to use fragments
// of this code in your own applications if you find them to be useful.  This class,
// along with the source code, come with no warranty of any kind, and the user assumes
// all responsibility for its use.
// -------------------------------------------------------------------------------------

#import <appkit/appkit.h>
#import <libc.h>
#import <stdlib.h>
#import <c.h>
#import <errno.h>
#import <ctype.h>
#import <math.h>
#import <sys/param.h>
#import <sys/types.h>
#import <sys/time.h>
#import <sys/wait.h>
#import <sys/resource.h>
#import "RemoteCommand.h"
#import "userInfo.h"
#import "ExecServer.h"
#import "ExecScrollText.h"
#import "ExecMonitor.h"

// -------------------------------------------------------------------------------------
// misc defines
#define	REMOTE_SERVER_CMD	"RemoteRunServer"

// -------------------------------------------------------------------------------------
// misc defines
#define	X					origin.x
#define	Y					origin.y
#define	W					size.width
#define	H					size.height
#define	freeString(X)		{ if (X) { free((char*)X); X = 0; } }
#define	freeCopy(T,F)		{ freeString(T); if (F) T = NXCopyStringBuffer(F); }
#define	cpnil				(char*)nil
#define	exeBtnTITLE			"Execute\nCommand"

// -------------------------------------------------------------------------------------
// ExecMonitor list of instances
static id					instanceList = (id)nil;

// -------------------------------------------------------------------------------------
// list of cached user passwords
typedef struct userPassword_s {
	char					*user;			// user name
	char					*password;		// password
	void					*next;			// pointer to next user
} userPassword_t;
static userPassword_t		*userList = (userPassword_t*)nil;

// -------------------------------------------------------------------------------------
@implementation ExecMonitor

// -------------------------------------------------------------------------------------
// user password
// -------------------------------------------------------------------------------------

/* find user password */
+ (userPassword_t*)_findUserRcd:(const char*)user
{
	userPassword_t	*u;
	for (u = userList; u && strcmp(user, u->user); u = (userPassword_t*)u->next);
	return u;
}

/* update user password */
+ (userPassword_t*)_addUser:(const char*)userName password:(const char*)password
{
	userPassword_t	*u;
	if (!XUserVerifyPassword(userName, password)) return (userPassword_t*)nil;
	u = (userPassword_t*)malloc(sizeof(userPassword_t));
	u->user = NXCopyStringBuffer(userName);
	u->password = NXCopyStringBuffer(password?password:"");
	u->next = (void*)userList;
	userList = u;
	return u;
}

/* find user password */
+ (const char*)findUserPassword:(const char*)user
{
	userPassword_t	*u = [self _findUserRcd:user];
	return u? u->password : (char*)nil;
}

/* update user password */
+ updateUser:(const char*)user password:(const char*)password
{
	userPassword_t	*u = [self _findUserRcd:user];
	if (u) { if (strcmp(u->password, password)) freeCopy(u->password, password); }
	else [self _addUser:user password:password];
	return self;
}

/* update user password */
+ removeUserPassword:(const char*)user
{
	userPassword_t	*u = userList, *l = (userPassword_t*)nil;
	for (;u && strcmp(user,u->user); l = u, u = (userPassword_t*)u->next);
	if (u) {
		(l? l->next : userList) = u->next;
		freeString(u->user);
		freeString(u->password);
		free(u);
	}
	return self;
}

// -------------------------------------------------------------------------------------
// object initialization
// -------------------------------------------------------------------------------------

/* new exec monitor */
+ newExecHost:(const char*)host server:(const char*)server
{
	self = [[self alloc] init];
	if (![self setRemoteHost:host server:server]) {
		[self free];
		return (id)nil;
	}
	[self showPanel:self];
	return self;
}

/* init */
- init
{
	NXRect	wFrame;
	
	/* instanceList initialization */
	if (!instanceList) instanceList = [[[List alloc] initCount:1] empty];
	[instanceList addObject:self];
	
	/* init super */
	[super init];
	exeUser = (id)nil;
	exePassword = (id)nil;
	exeWindow = (id)nil;
	exeCommandScroll = (id)nil;
	
	/* load nib */
	if (![NXApp loadNibSection:"ExecMonitor.nib" owner:self]) {
		NXLogError("[ExecMonitor] Could not load nib file 'ExecMonitor.nib'");
		[NXApp delayedFree:self];
		return self;
	}
	
	/* spot-check nib loading */
	if (!exeWindow || !exeCommandScroll) {
		NXLogError("[ExecMonitor] 'ExecMonitor.nib' did not load properly");
		[NXApp delayedFree:self];
		return self;
	}

	/* init vars */
	saveFileName = (char*)nil;
	useRunServer = YES;
	cmdInProcess = NO;
	shutDown     = NO;
	exeRunServer = (id)nil;
	
	/* set user values if specified */
	if (exeUser) [exeUser setStringValue:""];
	if (exePassword) [exePassword setStringValue:""];
	
	/* init panel */
	[exeExecute setTitle:exeBtnTITLE];
	[exeCommandScroll clearScrollText];
	[exeControlBox setBorderType:NX_NOBORDER];
	[exeWindow setDelegate:self];
	[exeWindow setFreeWhenClosed:NO];
	[exeWindow getFrame:&wFrame];
	minWinSize = wFrame.size;
	
	return self;
}

/* free */
- _free:sender { return [self free]; }
- free
{
	shutDown = YES;
	if (cmdInProcess) {	// loop until command has completed
		[self exeExecute:(id)nil];
		[self perform:@selector(_free:) with:self afterDelay:500 cancelPrevious:YES];
		return (id)nil;
	}
	[exeWindow free];
	[instanceList removeObject:self];	// make sure 'self' is removed
	return [super free];
}

// -------------------------------------------------------------------------------------
// run arguments

/* set remote host (start server if necessary) */
- setRemoteHost:(const char*)host server:(const char*)server
{
	char	cmd[MAXPATHLEN + 1], title[256];

	/* check for already set */
	if (exeRunServer) {
		NXLogError("[ExecMonitor] ExecServer is already running");
		return (id)nil;
	}
	
	/* instantiate ExecServer object */
	exeRunServer = (host && *host)? [[ExecServer alloc] init] : (id)nil;
	if (!exeRunServer) {
		NXLogError("[ExecMonitor] Could not create ExecServer (no host specified)");
		return (id)nil;
	}
	
	/* set attributes */
	[exeRunServer setMainAppPath:XAppPath()];
	[exeRunServer setMainAppServerName:[NXApp appServerName] host:[NXApp appServerHost]];
	[exeRunServer setRemoteHost:host];
	if (server && *server) [exeRunServer setRemoteServerName:server];
	sprintf(cmd, "%s/%s", XAppPath(), REMOTE_SERVER_CMD);
	if (![exeRunServer setServerCommandName:cmd]) {
		NXLogError("[ExecMonitor] Unable to set ExecServer command to %s", cmd);
		exeRunServer = (id)nil;
		return (id)nil;
	}
	
	/* start server */
	if (![exeRunServer startServer]) {
		NXLogError("[ExecMonitor] Could not connect to RemoteRunServer");
		exeRunServer = (id)nil;
		return (id)nil;
	}

	/* set window title */
	sprintf(title, "ExecMonitor: %s (%s)",
		[exeRunServer remoteHost], [exeRunServer remoteServerName]);
	[exeWindow setTitle:title];
	
	return self;
}

/* show ExecMonitor panel */
- showPanel:sender
{
	return [exeWindow makeKeyAndOrderFront:(id)nil];
}

// -------------------------------------------------------------------------------------
// command execution
// -------------------------------------------------------------------------------------

/* print messages */
- _printCompletion:(BOOL)isError:(char*)fmt, ...
{
	char	*h = isError?">>>>>>>>>>":"----------", *t = isError?"<<<<<<<<<<":"----------";
	va_list	args;
	
	/* message color and header */
	if (isError) [exeShellScroll setTextAttributeColor:NX_COLORRED];
	else [exeShellScroll setTextAttributeGray:NX_DKGRAY];
	[exeShellScroll textPrintf:"\n%s ", h];
	
	/* message */
	va_start(args, fmt);
	[exeShellScroll textPrintf:fmt args:args];
	va_end(args);
	
	/* message trailer and reset gray */
	[exeShellScroll textPrintf:" %s\n\n", t];
	[exeShellScroll setTextAttributeGray:textGray];
	
	/* return error condition */
	return isError? (id)nil : self;

}

/* execute Server command */
- exeExecute:sender
{
	int		commandLen;
	char	*cmdStr, *user, *pass;

	/* check for no run server */
	if (!exeRunServer) { NXBeep(); return (id)nil; }
	
	/* check for "STOP" */
	if (cmdInProcess) {
		if (!strcmp([exeExecute icon], "Stop")) {
			if (useRunServer) [exeRunServer terminateCommand:execId];
			else [exeShellScroll terminateCommand];
			[exeExecute setIcon:"Kill"];
		} else {
			if (useRunServer) [exeRunServer killCommand:execId];
			else [exeShellScroll killCommand];
		}
		return self;
	}
	if (!sender) return (id)nil;

	/* exit if shutdown in progress */
	if (shutDown) return (id)nil;
	
	/* get user */
	user = exeUser? (char*)[exeUser stringValue] : (char*)nil;
	if (!user || !*user) user = (char*)[NXApp appUserName];
	
	/* get password (if needed) */
	if (!XIsCurrentUser(user)) {
		pass = exePassword? (char*)[exePassword stringValue] : "";
		if ([exeRunServer needUserPassword:user] && !XUserVerifyPassword(user, pass)) {
			[[self class] removeUserPassword:user];
			return [self _printCompletion:1:"User password not valid"];
		}
		[[self class] updateUser:user password:pass];
	}

	/* setup command */
	commandLen = [[exeCommandScroll docView] textLength] + 1;
	cmdStr = (char*)malloc(commandLen + 1);
	[[exeCommandScroll docView] getSubstring:cmdStr start:0 length:commandLen];
	strcat(cmdStr, "\n");
	
	/* print command info */
	[exeShellScroll setTextAttributeGray:NX_DKGRAY];
	[exeShellScroll textPrintf:"\n---------- Executing Command ----------\n"];
	[exeShellScroll textPrintf:"%s", cmdStr];
	[exeShellScroll textPrintf:  "---------------------------------------\n\n"];
	[exeShellScroll setTextAttributeGray:textGray];
	
	/* run command */
	if (useRunServer) {
		execId = [exeRunServer runCommand:cmdStr
					withUser:user:[[self class] findUserPassword:user]
					forClient:exeShellScroll
					killOnError:YES];
	} else {
		execId = (execHandle_t)[exeShellScroll runCommand:cmdStr];
	}
	
	/* free command */
	free(cmdStr);

	/* check to see if command actually started running */
	if (!execId) return [self _printCompletion:1:"Command failed to start"];

	/* change button */
	cmdInProcess = YES;
	[exeExecute setIconPosition:NX_ICONONLY];
	[exeExecute setIcon:"Stop"];
	
	return self;
}

/* call-back from shell command (main thread) */
- commandDidComplete:shellId withError:(int)theError
{

	/* check for completion status */
	if (!theError) {	// successful
		[self _printCompletion:0:"completed normally"];
	} else {
		char *desc = [ExecServer errorDesc:theError];
		if (desc) [self _printCompletion:1:"%s",desc];
		else [self _printCompletion:1:"Terminated with exit(%d)",theError];
	}

	/* remove user if password is bad */
	if ((theError == RSRV_BADPASSWD) && exeUser) {
		[ExecMonitor removeUserPassword:[exeUser stringValue]];
	}

	/* reset execution button */
	[exeExecute setEnabled:YES];
	[exeExecute setIconPosition:NX_TITLEONLY];
	[exeExecute setTitle:exeBtnTITLE];
	cmdInProcess = NO;
	execId = (execHandle_t)nil;
	
	return self;
}

// -------------------------------------------------------------------------------------
// Terminal invoke
// -------------------------------------------------------------------------------------

/* invoke command in Terminal window */
+ terminalCommand:(const char*)cmd title:(const char*)title
{
	Speaker	*speaker;
    port_t	terminalPort;
    
	/* can't find Terminal */
	if (!(terminalPort = NXPortFromName("Terminal", NULL))) return (id)nil;
	
	/* launch Terminal */
	[NXApp deactivateSelf];
	creat("/tmp/.reallyignorethis.term", 0444);
	[[Application workspace] openFile:"/tmp/.reallyignorethis.term"
		fromImage:(id)nil at:(NXPoint*)nil inView:(id)nil];
	
	/* run command */
	speaker = [NXApp appSpeaker];
	[speaker setSendPort: terminalPort];
	[speaker selectorRPC:"runCommand:usingShell:inFolder:windowTitle:closeOnExit:"
		paramTypes: "cccci", cmd, "", "", title, NO];

	return self;
	
}

// -------------------------------------------------------------------------------------
// outlets
// -------------------------------------------------------------------------------------

- setExeShellScroll:anObject
{
	exeShellScroll = [[ExecScrollText newExecScrollText:anObject] clearScrollText];
	[exeShellScroll setDelegate:self];
	textFont = [[exeShellScroll docView] font];
	textGray = [[exeShellScroll docView] textGray];
	[exeShellScroll setTab:[textFont getWidthOf:"        "] count:10];
	[exeShellScroll textPrintf:"\n"];	// this forces the scroll view to initialize
	[exeShellScroll clearScrollText];
	return self;
}

- setExeCommandScroll:anObject
{
	exeCommandScroll = [[ExecScrollText newExecScrollText:anObject] clearScrollText];
	return self;
}

// -------------------------------------------------------------------------------------
// first responder methods
// -------------------------------------------------------------------------------------

/* save error handler */
- (BOOL)attemptOverwrite:(const char*)fileName
{
	NXLogError("[ExecMonitor] Unable to save %s", fileName);
	return NO;
}

/* save contents of text view */
- saveAs:sender
{
	char	*temp;
	id		savePanel = [SavePanel new];

	/* set up save panel */
	[savePanel setTitle:"Save Command Output"];
	[savePanel setPrompt:"File:"];
	[savePanel setRequiredFileType:""];
	[savePanel setDirectory:(saveFileName?saveFileName:[NXApp appUserHome])];

	/* get file name to save */
	if (![savePanel runModalForDirectory:[NXApp appUserHome] file:""]) return self;
	if (!(temp = (char*)[savePanel filename])) return self;
	freeString(saveFileName);
	saveFileName = NXCopyStringBuffer(temp);
	
	return [self save:sender];
}

/* save contents of text view */
- save:sender
{
	if (!saveFileName) return [self saveAs:sender];
	[[exeShellScroll docView] saveRTFDTo:saveFileName removeBackup:NO errorHandler:self];
	return self;
}

// -------------------------------------------------------------------------------------
// window close
// -------------------------------------------------------------------------------------

/* close all windows */
+ closeAllWindows
{
	if (instanceList) [instanceList makeObjectsPerform:@selector(closeWindow:) with:self];
	return self;
}

/* shut down */
- closeWindow:sender
{
	return [exeWindow performClose:self];
}

// -------------------------------------------------------------------------------------
// window delegate methods
// -------------------------------------------------------------------------------------

- windowWillClose:windowId
{
	if (cmdInProcess) return (id)nil;
	[instanceList removeObject:self];
	return [NXApp delayedFree:self];
}

- windowWillResize:windowId toSize:(NXSize*)newSize
{
	if (newSize->width  < minWinSize.width ) newSize->width  = minWinSize.width;
	if (newSize->height < minWinSize.height) newSize->height = minWinSize.height;	
	return self;
}

@end
